commonlibsse_ng\re\b/
BSFixedString.rs

1use crate::re::BSStringPool::{self, StringFormat, U16};
2use core::{marker::PhantomData, mem, ptr};
3
4/// A fixed-length internal string representation used for interacting with
5/// the `BSStringPool`.
6///
7/// This is similar to `::core::ffi::CStr`, but supports both `char` and `wchar_t` formats
8/// through the `StringFormat` trait.
9///
10/// # Type Parameters
11/// - `T`: The string format (`U8` for `char` or `U16` for `wchar_t`).
12///
13/// # Safety
14/// Since this is an FFI type, the encoding is not guaranteed. It may be UTF-8, ANSI,
15/// UTF-16LE, or platform-specific wide encoding.
16#[repr(transparent)]
17pub struct BSFixedStringInternal<T>
18where
19    T: StringFormat,
20{
21    /// Pointer to the string data (null-terminated).
22    data: *const T::Unit,
23    marker: PhantomData<Box<T::Unit>>,
24}
25
26impl<T> BSFixedStringInternal<T>
27where
28    T: StringFormat,
29{
30    /// Tries to acquire a reference to the string in the string pool.
31    ///
32    /// If the string is already in the pool, its reference count is incremented.
33    pub fn try_acquire(&self) {
34        if let Some(proxy) = unsafe { self.get_proxy() } {
35            proxy.acquire();
36        }
37    }
38
39    /// Tries to release the string from the string pool.
40    ///
41    /// If the string is not null, it decrements its reference count.
42    /// If the reference count reaches zero, the string is removed from the pool.
43    pub fn try_release(&mut self) {
44        if !self.data.is_null() {
45            unsafe { BSStringPool::Entry::<T>::release(&self.data) };
46            self.data = ptr::null();
47        }
48    }
49
50    /// Gets a mutable reference to the `BSStringPool::Entry`.
51    ///
52    /// Returns `None` if the string data is null.
53    ///
54    /// # Safety
55    /// This dereferences a pointer that may point to invalid memory.
56    pub const unsafe fn get_proxy(&self) -> Option<&mut BSStringPool::Entry<T>> {
57        if self.data.is_null() {
58            return None;
59        }
60
61        let proxy_ptr = unsafe { self.data.sub(mem::size_of::<BSStringPool::Entry<T>>()) }
62            as *mut BSStringPool::Entry<T>;
63        unsafe { proxy_ptr.as_mut() }
64    }
65
66    /// Returns the length of the string.
67    ///
68    /// Returns `0` if the string data is null or the proxy is invalid.
69    #[inline]
70    pub fn count_bytes_with_null(&self) -> u32 {
71        unsafe { self.get_proxy().map_or(0, |proxy| proxy.len()) }
72    }
73
74    /// Checks whether the string is empty.
75    ///
76    /// Returns `true` if the string length is zero.
77    #[inline]
78    pub fn is_empty(&self) -> bool {
79        self.count_bytes_with_null() == 0
80    }
81
82    /// Converts this C string as a byte slice with null.
83    #[inline]
84    pub fn as_bytes_with_null(&self) -> &[u8] {
85        unsafe {
86            core::slice::from_raw_parts(
87                self.data.cast::<u8>(),
88                self.count_bytes_with_null() as usize,
89            )
90        }
91    }
92}
93
94impl<T: StringFormat> Clone for BSFixedStringInternal<T> {
95    #[inline]
96    fn clone(&self) -> Self {
97        let cloned = Self { data: self.data, marker: PhantomData };
98        cloned.try_acquire();
99        cloned
100    }
101}
102
103impl PartialOrd for BSFixedString {
104    #[inline]
105    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
106        Some(self.cmp(other))
107    }
108}
109
110impl Ord for BSFixedString {
111    #[inline]
112    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
113        self.as_bytes_with_null().cmp(other.as_bytes_with_null())
114    }
115}
116
117impl core::hash::Hash for BSFixedString {
118    #[inline]
119    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
120        self.as_bytes_with_null().hash(state);
121    }
122}
123
124impl<T: StringFormat> Drop for BSFixedStringInternal<T> {
125    #[inline]
126    fn drop(&mut self) {
127        self.try_release();
128    }
129}
130
131pub use u8_bytes::{BSFixedString, ctor8};
132pub use u16_wide::{BSFixedStringW, ctor16};
133
134mod u8_bytes {
135    use super::*;
136    use crate::re::BSStringPool::U8;
137    use core::ffi::{CStr, c_char};
138    use core::ops::Deref;
139    use core::{fmt, str};
140
141    /// A fixed-length C string.
142    ///
143    /// This type is string-interlaced and reference-shares memory with the same string.
144    ///
145    /// # Encoding
146    /// Since this is an FFI type, the encoding is not guaranteed. It may be UTF-8, ANSI,
147    /// or some other platform-specific encoding, as it uses `char` internally.
148    ///
149    /// Therefore, when using this string, it is recommended to convert it to `&str`
150    /// to handle it safely.
151    ///
152    /// - ref: [`Converting esp/string Encoding`](https://www.nexusmods.com/skyrimspecialedition/articles/32/)
153    pub type BSFixedString = BSFixedStringInternal<U8>;
154
155    /// Creates a new `BSFixedStringInternal` from a raw pointer.
156    ///
157    /// # Safety
158    /// - `data` must be a valid null-terminated string pointer.
159    #[commonlibsse_ng_derive_internal::relocate_fn(se_id = 67819, ae_id = 69161)]
160    #[allow(clippy::use_self)]
161    pub unsafe fn ctor8(data: *const c_char) -> BSFixedString {}
162
163    /// A constant pointer to an empty string.
164    pub const EMPTY_C_CHAR: *const c_char = c"".as_ptr();
165
166    impl BSFixedString {
167        /// const default
168        pub const DEFAULT: Self = Self { data: EMPTY_C_CHAR, marker: PhantomData };
169
170        /// Creates a new `BSFixedString` from a `&CStr`.
171        ///
172        /// ## Memory allocation
173        /// - If the same string exists inside the allocator, no new memory allocation occurs.
174        #[inline]
175        pub fn new(data: &CStr) -> Self {
176            unsafe { Self::new_unchecked(data.as_ptr()) }
177        }
178
179        /// Creates a new `BSFixedString` from a raw pointer.
180        ///
181        /// # Safety
182        /// - `data` must be a valid null-terminated `char` string.
183        ///
184        /// ## Memory allocation
185        /// - If the same string exists inside the allocator, no new memory allocation occurs.
186        #[inline]
187        pub unsafe fn new_unchecked(data: *const c_char) -> Self {
188            unsafe { ctor8(data) }
189        }
190
191        /// Gets the string as a `CStr`.
192        ///
193        /// # Encoding
194        /// The returned string may not be UTF-8 due to its FFI nature.
195        /// It is safe to use this as a raw `CStr`, but converting to `str` should be done carefully.
196        pub fn as_c_str(&self) -> &CStr {
197            if let Some(proxy) = unsafe { self.get_proxy() } {
198                unsafe {
199                    return CStr::from_ptr(proxy.as_raw());
200                }
201            }
202            unsafe { CStr::from_ptr(EMPTY_C_CHAR) }
203        }
204
205        /// Converts the string to `&str` if it is valid UTF-8.
206        #[inline]
207        pub fn to_str(&self) -> Option<&str> {
208            core::str::from_utf8(self.as_bytes_with_null()).ok()
209        }
210
211        /// Returns true if `CStr` passed as argument is contained in this string or not.
212        #[inline]
213        pub fn contains(&self, rhs: &CStr) -> bool {
214            let self_bytes = self.as_bytes_with_null();
215            let rhs_bytes = rhs.to_bytes();
216            let rhs_len = rhs_bytes.len();
217
218            if rhs_len > self_bytes.len() {
219                return false;
220            }
221
222            self_bytes.windows(rhs_len).any(|window| window == rhs_bytes)
223        }
224    }
225
226    impl PartialEq for BSFixedString {
227        #[inline]
228        fn eq(&self, other: &Self) -> bool {
229            if self.is_empty() && other.is_empty() {
230                true
231            } else {
232                self.as_c_str() == other.as_c_str()
233            }
234        }
235    }
236
237    impl Eq for BSFixedString {}
238
239    impl Default for BSFixedString {
240        #[inline]
241        fn default() -> Self {
242            Self { data: EMPTY_C_CHAR, marker: PhantomData }
243        }
244    }
245
246    impl Deref for BSFixedString {
247        type Target = CStr;
248
249        #[inline]
250        fn deref(&self) -> &Self::Target {
251            self.as_c_str()
252        }
253    }
254
255    impl fmt::Display for BSFixedString {
256        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257            write!(f, "{}", self.as_c_str().to_string_lossy())
258        }
259    }
260
261    impl fmt::Debug for BSFixedString {
262        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263            write!(
264                f,
265                "{:?}",
266                str::from_utf8(self.as_bytes_with_null()).unwrap_or("<Invalid UTF-8>")
267            )
268        }
269    }
270
271    impl From<&CStr> for BSFixedString {
272        fn from(value: &CStr) -> Self {
273            Self::new(value)
274        }
275    }
276}
277
278mod u16_wide {
279    use super::*;
280    use core::fmt;
281    use std::string::FromUtf16Error;
282
283    /// Fixed length wide C string.
284    ///
285    /// # Encoding
286    /// Since this is an FFI type, the encoding is not guaranteed. It may be UTF-16LE,
287    /// UTF-32LE, or platform-specific wide encoding, as it uses `wchar_t` internally.
288    ///
289    /// Therefore, when using this string, it is recommended to convert it to `&str`
290    /// to handle it safely.
291    pub type BSFixedStringW = BSFixedStringInternal<U16>;
292
293    /// Creates a new `BSFixedStringW` from a pointer
294    #[commonlibsse_ng_derive_internal::relocate_fn(se_id = 67834, ae_id = 69176)]
295    #[allow(clippy::use_self)]
296    pub unsafe fn ctor16(data: *const u16) -> BSFixedStringW {}
297
298    impl BSFixedStringW {
299        /// A constant pointer to an empty string.
300        pub const EMPTY: &'static [u16] = &[];
301
302        /// # Safety
303        /// `data` must be a pointer to a null-terminated UTF-16LE string.
304        #[inline]
305        pub const unsafe fn new_unchecked(data: *const u16) -> Self {
306            if !data.is_null() {
307                Self { data, marker: PhantomData }
308            } else {
309                Self { data: Self::EMPTY.as_ptr(), marker: PhantomData }
310            }
311        }
312
313        /// Convert as `[u16]`
314        #[inline]
315        pub fn as_wide(&self) -> &[u16] {
316            unsafe { self.get_proxy() }.map_or(&[], |proxy| unsafe {
317                core::slice::from_raw_parts(proxy.as_raw(), proxy.len() as usize)
318            })
319        }
320
321        /// Decode a UTF-16–encoded slice v into a String
322        #[inline]
323        pub fn to_string_lossy(&self) -> String {
324            String::from_utf16_lossy(self.as_wide())
325        }
326
327        /// Decode a UTF-16–encoded vector v into a String.
328        /// # Errors
329        /// Invalid UTF-16 encoding
330        #[inline]
331        pub fn to_string(&self) -> Result<String, FromUtf16Error> {
332            String::from_utf16(self.as_wide())
333        }
334    }
335
336    impl PartialEq for BSFixedStringW {
337        #[inline]
338        fn eq(&self, other: &Self) -> bool {
339            self.as_wide() == other.as_wide()
340        }
341    }
342
343    impl Default for BSFixedStringW {
344        #[inline]
345        fn default() -> Self {
346            Self { data: Self::EMPTY.as_ptr(), marker: PhantomData }
347        }
348    }
349
350    impl fmt::Display for BSFixedStringW {
351        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352            write!(f, "{}", self.to_string_lossy())
353        }
354    }
355
356    impl fmt::Debug for BSFixedStringW {
357        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358            write!(f, "{:?}", self.to_string_lossy())
359        }
360    }
361}
362
363// #[cfg(test)]
364// mod tests {
365//     use super::*;
366
367//     #[test]
368//     fn test_bs_string() {
369//         let mut bs_fixed_string = unsafe { BSFixedString::new_unchecked(c"Hello World".as_ptr()) };
370//         assert_eq!(bs_fixed_string.count_bytes(), 12);
371//         assert_eq!(bs_fixed_string.to_str(), Some("Hello World"));
372//         assert!(bs_fixed_string.contains(c"World"));
373
374//         bs_fixed_string.try_release();
375//         assert!(bs_fixed_string.data.is_null());
376//     }
377// }